Conversation
|
Lexical Graph Coverage Report: The coverage is at unknown% (target: unknown%). Download the HTML report here. |
a4e9720 to
3091051
Compare
|
Lexical Graph Coverage Report: The coverage is at unknown% (target: unknown%). Download the HTML report here. |
Lets a user supply an OWL/RDFS ontology as the extraction vocabulary, so that a graph names the same idea one way rather than several. Opt-in: with no ontology configured, the emitted graph queries and the extraction prompts are byte-identical to a build from before this change. Two mechanisms. The ontology's classes and properties, with their comments, are rendered into the extraction prompts, so the model is told the vocabulary instead of inventing one. After extraction, a deterministic transform component resolves each emitted name against the ontology and - depending on the authority the ontology is given - rewrites it to the authored spelling, or drops what does not conform. One setting spans that axis. `ontology_authority='off'` says nothing; `'align'`, the default, stores a matched concept under the ontology's name and keeps what the ontology does not declare; `'strict'` also excludes what does not conform. The level resolves to six independent dimensions, each individually overridable, so a single gate can be asked for by name. Optionally, values of declared datatype properties are coerced to their declared XSD type and written onto entity nodes as native graph properties, which is what makes a numeric or date comparison possible in a query. Coercion is lenient about lexical form - digit grouping, named-month dates - and strict about meaning: a literal that would need interpreting rather than parsing is refused rather than stored under a key whose name promises a number. The new code is one package, indexing/extract/ontology/: loading and validation, a plain-data read index that crosses the extraction process boundary, prompt rendering, XSD coercion, and the filter. Everything else is small edits at the call sites. Also included, and unrelated: pytest's default norecursedirs contains "build", so tests/unit/indexing/build/ had never been collected in a full-suite or CI run. Removing it surfaced six builder test files that no longer passed, plus five tests elsewhere that were already failing on main. Documented at docs-site/.../lexical-graph/ontology-guided-extraction.mdx.
3091051 to
4e625b3
Compare
|
Lexical Graph Coverage Report: The coverage is at 73.7% (target: 80%). Download the HTML report here. |
aghassel
left a comment
There was a problem hiding this comment.
AI-generated review. Reviewed manually before posting.
Nice piece of work. Because the feature is opt-in and the no-ontology path really is untouched, none of the below affects
existing users. Everything is scoped to people who turn it on. Two things I'd like to see fixed before
merge, and a family of four that I think matters more than its individual severity suggests:
Blocking
-
typed_properties='complement'/'both'silently produces nothing — the carry clause added to
copy_complement_relationships_to_subjectnever executes, and the complement'styped_valueis
DETACH DELETEd with the node. 13 passing tests assert the query's text, so they certify the dead
path. Detail inline. -
entityIdis missing fromRESERVED_ENTITY_PROPERTIES, so a subject typed write can overwrite the
__Entity__merge key on property-keyed stores. Detail inline.
The thing I'd most like to change about the feature as a whole: it has no loud failure mode.
report_violations defaults to False, and four separate paths make a misconfigured ontology
indistinguishable from a working one — an RDFS-dialect file loads to zero terms with no warning;
vocabulary_format='turtle' names classes the response parser cannot resolve back; enforce_*='off'
turns the gate on; and combining an ontology with infer_entity_classifications drops the ontology's
class names out of the prompt. Each is a few lines. Comments inline.
The rest of what I found is small and listed in one follow-up comment rather than scattered inline.
|
|
||
| if typed_properties in COMPLEMENT_PLACEMENTS: | ||
| (typed_value_key, datatype_key) = COMPLEMENT_ENTITY_PROPERTIES | ||
| carry_typed_values = ( |
There was a problem hiding this comment.
This carry never runs, so at typed_properties='complement'/'both' the coerced value is lost.
copy_complement_relationships_to_subject receives no rows in either write mode. Driving
GraphBatchClient over a recording store with the root query returning one row, the executed queries
were, in order:
get complements matching subject
delete complement relationships <- contains DETACH DELETE c
get subjects matching complement
delete complement relationships
The copy-with-carry query never executed; with batch_writes_enabled=False, nothing in the tree executed
at all. So the complement node — carrying the typed_value/datatype this PR just wrote onto it — is
deleted and nothing is carried onto the surviving subject.
test_local_entity_rewrites_typed_carry.py (13 tests, all passing) asserts on the generated query text,
so it certifies the clause exists rather than that it runs.
Two ways out: fix the execution defect (materialise the root results into a list before fanning out to
children in QueryTree.run, and iterate the generator on the non-batch path), or drop the clause and say
in the placement table that complement typed values don't survive the local-entity fold. Either way I'd
switch those tests to assert on the (query, params) pairs that actually reach the store through
GraphBatchClient — that's the assertion that would have caught this.
There was a problem hiding this comment.
You're right on every mechanical point here, and I verified each one separately:
- the carry clause never executes — confirmed;
copy_complement_relationships_to_subjectreceives no rows in either write mode —
confirmed;- with
batch_writes_enabled=False, nothing in the tree executes at all — confirmed; - the 13 tests assert generated query text and therefore certify a dead path — confirmed, and
a fair hit.
One thing in your favour that I don't think the comment claims: this isn't a regression in this
PR. The copy query, its wiring as a child_queries entry, and both QueryTree constructions
are byte-identical to main; the PR adds only the carry clause. query_tree.py,
graph_batch_client.py and graph_store.py — where the defect actually lives — aren't touched
here at all. I ran the same probe in a clean main worktree with no ontology code present and no
typed_properties kwarg: copy executes 0 times batched, nothing executes unbatched. So
local-entity folds have never moved complement relationships onto the surviving subject, for
anyone, ontology or not.
Where I'd push back is the conclusion — "at typed_properties='complement'/'both' the coerced
value is lost" as a general statement. Your repro drove the tree "with the root query returning
one row", which forces the fold to match. How often it matches on real input turns out to be the
whole question.
Root query 2 (get subjects matching complement) cannot match at all under the default
include_classification_in_entity_id=True. Its lookup key is
complement.altEntityId = create_entity_id(value, '__Local_Entity__'), but no node is ever
written with an id built from __Local_Entity__ as a classification — real entities hash their
real class, and complement nodes use create_local_entity_id, which is a different node-type
prefix and source-scoped:
create_entity_id('help@acme.test', '__Local_Entity__') = 57844660354b037e…
create_entity_id('help@acme.test', 'Person') = 53da0b24893139cb…
equal: False
So root query 1 is the only live path, and it needs a __Local_Entity__ node whose
search_str equals a real (non-local) subject entity's search_str — a genuine collision
between a value and an entity name.
Driving the real EntityGraphBuilder and LocalEntityRewritesGraphBuilder through a
GraphBatchClient, against a store that keeps node state and answers both root queries
faithfully rather than returning a fixed row:
A. Ordinary attribute values (1994, 1971, 41500000)
typed_value surviving: 3 deleted by the fold: 0
B. A value that is also a real entity ('help@acme.test' is a Person elsewhere)
typed_value surviving: 0 deleted by the fold: 1
So what leaving this alone actually costs:
| Placement | Cost |
|---|---|
'subject' |
None. The subject write is a separate plain-string query that batches normally and never touches the fold. This is the documented recommended placement. |
'complement' |
The typed value is lost only on a complement whose string is also extracted as a named entity — and that node is deleted from the graph today regardless, so nothing that survives is wrong. |
'both' |
Subject half unaffected; complement half as above. |
And it lands where the typed value is worth least: in case B the declared range is xsd:string,
so typed_value is a byte-for-byte copy of value. The ranges where coercion adds something —
integer, decimal, date, boolean — are the ones whose values are essentially never also entity
names.
On that basis I've left this untouched and I'd like to treat it as a documented edge case
rather than a blocker, with the underlying defect raised separately (issue to follow). I took your option
(a) — fix QueryTree.run and the unbatched path — seriously, and it's contained (that builder is
the only QueryTree consumer in src/), but it changes graphs produced by builds with no
ontology at all:
| Config | Default? | Impact of fixing QueryTree |
|---|---|---|
include_local_entities=False, batched |
yes | None. Query stream byte-identical — no __Local_Entity__ node exists, so both root queries match nothing |
include_local_entities=False, unbatched |
no | Extra round trips that write nothing (UNWIND []). Graph unchanged |
include_local_entities=True, batched |
no | Graph gains (s)-[:__RELATION__{value}]->(n) and (n)-[:__OBJECT__]->(f). Additive |
include_local_entities=True, unbatched |
no | The whole fold starts running; local-entity nodes get merged and DETACH DELETEd where they currently survive |
The default is safe, but that last row is more than a feature PR should carry. Happy to be argued
out of this if you think the silent edge loss is urgent enough to want it fixed in here.
Your testing point stands regardless, and I'm not trying to wriggle out of it: those 13 tests
assert query text, which is exactly what let a clause on a dead query read as a working
feature. I'd rather move them to asserting the (query, params) pairs that reach the store as
part of that follow-up, since the same assertions are what would prove the fix works — rather than
rewriting them here to assert the current dead behaviour, which would just bake the bug into the
suite.
Smaller things, roughly in the order I'd care about them. Happy to open issues instead if you prefer. Correctness
Security / robustness (author-supplied ontology, so low, but cheap)
Docs / hygiene
Method note: findings were reproduced against a real venv (rdflib 7.6.0, Python 3.12 plus a 3.10 for |
Correctness
- Reserve `entityId`: a typed subject write could key on the `__Entity__` merge
key, making the node unreachable by id on property-keyed stores (Neo4j,
FalkorDB). Assert against the emitted Cypher rather than the constant.
- Refuse dimension overrides that are not True/False/None. `bool('off')` is
True, so `enforce_entity_types='off'` turned the gate on - and `'off'` is a
valid value for three other settings on the same constructor.
- Refuse a zoned `xsd:dateTime`/`xsd:time` instead of stripping the offset,
which stored the same instant as different strings and 14h apart as identical
ones, both reported as conforming. `xsd:date` still drops it.
- Accept fractional seconds for `xsd:time`; the `(\.\d+)?` group was dead.
- Refuse a leading `-` on `xsd:date` rather than stripping the sign, which
stored 500 BCE as 500 CE.
- Union the ontology's class names into `InferClassifications` output, exempt
from truncation. They were dropped, so the prompt named classes that
`{preferred_entity_classifications}` did not, and `strict` then discarded what
inference produced.
- Resolve a class by a separator-free fallback key when the exact key misses,
so the ontology's own spelling works: the response parser title-cases
`SportsTeam` to `Sportsteam`. Ambiguous coarse keys are omitted, never
guessed, and the exact path is unchanged. Classes only - predicates already
round-trip.
- Accept `rdfs:subClassOf owl:Thing`, a legal axiom Protege emits; it was a
fatal dangling reference.
- Fold with `casefold()` rather than `lower()`, so a rendered name is the
inverse of its indexed key for non-ASCII (`Grosse`/`GROSSE`).
- Catch `OverflowError` from dateutil on Neptune; an out-of-range numeric
killed the build mid-batch.
- Clear `{ontology_constraints}` when the block is empty, instead of sending the
literal placeholder to the model.
- Fix batching: `new_query_var()` in the query text gave every typed write its
own round trip. 50 facts at batch_write_size=25: subject 52 -> 4, both
104 -> 8.
Diagnostics, where the feature had no loud failure mode
- Warn when a non-empty ontology declares no OWL terms, naming `rdfs:Class` and
`rdf:Property`. An RDFS-dialect file loaded to zero terms silently, then
dropped every fact at `strict`.
- Warn on colliding resolution keys, on multiple `rdfs:range` values, and on
multiple `rdfs:label`/`rdfs:comment` values, naming the winner in each case.
- Report `validates_datatype()` False for the nine string-derived XSD types
whose lexical space is never checked; only `xsd:string` is unconstrained.
Stored values are unchanged.
- Pickle-test the logging config before handing it to spawn workers, as the
config snapshot already does per field.
Reproducibility and robustness
- Renumber blank-node labels in the turtle vocabulary block, which rdflib mints
per process. The prompt is now byte-reproducible, so `LLMCache` stops missing.
- Make `xsd:anyURI` rejection linear. The pattern was cubic on the input it
exists to reject: 4 KB of dotted prose took 28s.
- Flatten `rdfs:comment` to one line, so a newline plus `##` cannot forge a
section heading in the rendered block.
Docs
- Correct the three claims that an `off` setting is a no-op; the ontology still
seeds the preferred classifications.
- Correct the reserved-name guard, including which placements it runs at.
- Document the load-time rules, `owl:imports` not being followed, and the two
cases that load clean and do nothing.
- State that every dimension gates facts only: statements with no surviving
facts, and `topic.entities`, are never pruned.
- Correct the rdflib-free import claims; the plain-data claim is the one holding.
- Untrack `lexical-graph/output.log` and ignore `output.log`.
|
Worked through all of these. Every one reproduced. Fourteen are fixed, nine are closed with a Where you were too generous:
|
| Item | Note |
|---|---|
datatype_utils.py:227 timezone discarded |
Zoned dateTime/time now refused, not stripped. Converting to UTC was considered and rejected — it rewrites what the text stated, which the prompt promises the model it will not do, and leaves a naive local time indistinguishable from a converted one. xsd:date still drops the offset, deliberately: the value there is a calendar date |
datatype_utils.py:242 fractional xsd:time |
(\.\d+)? is live now; xsd:time and xsd:dateTime agree on the same lexical form |
datatype_utils.py:210 leading - on _ISO_DATE |
Refused outright, matching what _coerce_datetime already did. The lstrip('-') is gone |
datatype_utils.py:133 _ANY_URI |
Above |
datatype_utils.py:259 string family |
Nine types removed from _COERCERS; they fall through to the existing unimplemented path, which already returns trimmed text and already warns. Stored values byte-identical. Detail below |
entity_graph_builder.py:364,417 round trips |
Fixed variable names. subject 52 → 4, both 104 → 8. insert_domain_entity keeps its uuid — pre-existing, and its own comment already flags batching as a separate optimisation |
ontology.py:537 subClassOf owl:Thing |
owl:Thing filtered out of parents, consistent with _class_reference already exempting it for domain and range. Your point about the workaround injecting a meaningless Thing type was the deciding one |
ontology.py:453 datatype=ranges[0] |
Warns on multiplicity, matching the sibling _class_reference warning one line above |
ontology.py:355 language tags |
Warns and names the winner. No language preference applied — picking @en would put an opinion about language in exactly one place with no setting to change it, and the author is better placed to resolve it in the ontology. The warning notes that for rdfs:label the winner becomes the canonical stored spelling |
ontology.py:275 blank-node instability |
Labels renumbered to _:b1, _:b2 in first-appearance order before the turtle reaches the prompt. Three processes now produce an identical block; previously three different sha256s. Skolemising would not have helped — it mints a fresh UUID too |
ontology_index.py:137 duplicate resolution keys |
Warns, naming the winner. Extra evidence for you: class_names() returns the duplicate, so a collision also seeds {preferred_entity_classifications} with a repeated name |
naming.py:97 .upper() / .lower() |
resolution_key folds with casefold(). Fixes Größe; Straße/Strasse now collide as one key rather than one being silently unresolvable, which the new collision warning reports |
neptune_graph_stores.py:174 OverflowError |
Added to the caught tuple, with tests for out-of-range numbers and non-strings |
prompts.py:303 literal {ontology_constraints} |
Cleared on the empty path. One existing test asserted the old behaviour; its rationale was avoiding a blank line, which is not worth sending a literal brace group to the model |
prompt_constraint.py:485 rdfs:comment |
Flattened to one line, so a newline plus ## can no longer forge a heading. Reproduced your two ## Using this vocabulary headings first. The test asserts on line starts, not substrings — flattening demotes the forged heading to prose rather than deleting the words |
| All four docs/hygiene items | Below |
Closed, with reasons
| Item | Why |
|---|---|
ontology.py:526 range validated by prefix only |
Already audible: a misspelt range produces enforce_datatypes cannot validate …#Integer, declared as the range of p. The code path is shared with deliberate text fallbacks, as you say, but the diagnostic is not |
neptune_graph_stores.py:170 suffix vs rdfs:range |
Threading the range through property_assigment_fn is a public-interface change touching every store implementation and caller. And the existing fallback provably absorbs it: int/float/bool raise TypeError, non-date text raises ParserError, so the wrapper only applies where it is meaningful. Documented instead — the docs now say the decision is made on the name alone |
ontology_filter.py:723 object side escapes both gates |
"Unknown is not violating" is documented design, and there is genuinely no object class to test: the parser never matched the object text to an entity, so the fact has a literal complement. Not fixable without changing what the parser emits. Documented in the authority table |
ontology_filter.py:446 statements survive with facts: 0 |
Reproduced — statement kept, text still in the vector index. Scoped the claim rather than pruning: the authority table now states that every dimension gates facts and only facts, that such a statement still reaches the graph and the statement vector index, and that topic.entities is never pruned either |
pipeline_utils.py:72 report unreachable under basicConfig |
Not a regression — origin/main passed no logging config to workers at all, so this PR strictly improved things and the gap only affects users who were no better off before. And the fix is worse than the gap: basicConfig(level=INFO) inside a worker turns on INFO for every component to deliver one line per batch. Documented as requiring set_logging_config |
pipeline_utils.py:77 dictConfig not pickle-tested |
Fixed, not closed — it now pickle-tests and degrades to None with a warning, matching what config_snapshot does per field |
On the string family
Your framing was right and the fix was smaller than expected. Of the ten, only xsd:string is
genuinely unconstrained — returning the literal unchanged is the check for it. The other nine
have restricted lexical spaces that nothing here checks. Removing them from _COERCERS is the
whole change: they fall through to the unimplemented-type path, which already returns trimmed text
and already warns once per type, so stored values are identical and the user is simply told.
I chose not to implement the lexical checks. At strict a failing value is dropped, and losing a
fact because a token carried two consecutive spaces is a worse outcome than storing it
unchecked. These types document intent; they are not worth making into a gate.
Also fixed the validates_datatype docstring, which claimed True "for every XSD type with an
implementation, including the string family" — the clause that made this look intentional.
Docs and hygiene
- The three "
offis a no-op" claims: corrected.'off'still seeds
{preferred_entity_classifications}, and the placement table's byte-identical claim now scopes
to the emitted queries. graph-model.mdxreserved names: rewritten, including which placements the check runs at.- Writing an ontology: new "What is checked when the file loads" subsection covering every
load-time rule,owl:importsnot being followed, and the two cases that load clean and do
nothing. - The rdflib-free import claim: corrected in three comments. You are right that the plain-data
claim is the one that holds — confirmed that importingontology_indexalone leavesrdflib
insys.modules, becauseindexing.extract.__init__re-exports the package. lexical-graph/output.log: untracked, andoutput.logadded to.gitignore.
Two grammar decisions I left to Python
_coerce_datetime still has no shape regex, deliberately. I tested 3.10 rather than assume: the
divergence is confined to fractional-second digit counts other than 3 or 6, ISO basic format, and
week dates. Every form a model realistically emits behaves identically on both. Pinning that would
mean owning a date parser, so it is documented instead, including that a strict build on 3.10
can retain marginally fewer facts.
Thanks for the method note at the end of your comment — reproducing against a real venv rather
than reading the diff is why almost all of this was actionable without a round of argument.
Flattening newlines closed one of the three vectors in the review; these are the other two. - Size the turtle fence to the content. A comment carrying a run of backticks is serialized verbatim, so a fixed three-backtick fence was closed early by it, putting the rest of the ontology and the protocol section after it outside the code block. One more backtick than the longest run, per CommonMark. - Replace `|` in a description. It is the response protocol's field separator - the prompt asks for `entity|RELATIONSHIP|entity` and `parse_extracted_topics` splits on it - so a `|` from a comment reads to the model as a worked example of the output it is being asked to produce. A slash rather than a deletion, so "revenue|turnover" still reads as the alternation the author meant. Neither is escaped: this is prose fed to a model, not a format with an escape syntax.
|
Correction to my previous comment, on I listed that item under "Fixed" without qualifying it, and only one of the three vectors you So a comment carrying ``` did close the fence early — putting the rest of the ontology and the Both now fixed in
Neither is escaped, because there is nothing to escape to — this is prose fed to a model, not a While I am correcting the record, one other qualifier I should have made explicit: on |
acarbonetto
left a comment
There was a problem hiding this comment.
This looks like a nice contribution.
| block = _neutralize_format_fields(constraints) | ||
|
|
||
| if ONTOLOGY_CONSTRAINTS_PLACEHOLDER in template: | ||
| return template.replace(ONTOLOGY_CONSTRAINTS_PLACEHOLDER, block) |
There was a problem hiding this comment.
The {ontology_constraints} placeholder path uses template.replace(...) (all occurrences) while the anchor path one group below uses count=1. Was this intended?
Title: [FEATURE] Ontology-guided extraction and typing
Description
Two related capabilities, both opt-in and both driven from an OWL/RDFS ontology you supply.
Extraction guided by a vocabulary. The ontology's entity types, permitted relationships
and attributes are rendered into the extraction prompts, and a deterministic filter then
resolves what the model emitted against the ontology — so a graph names the same idea one
way rather than several.
Companyrather thanCompanyandCORPORATION;worksForrather than
WORKS_FORandEMPLOYED BY.Typing of extracted values. Values of declared datatype properties are coerced to their
declared XSD type and written onto entity nodes as native graph properties, so
WHERE c.foundedYear < 2000becomes a query you can run against the graph rather thansomething only an LLM can answer from text.
With no ontology configured, the emitted graph queries and the extraction prompts are
byte-identical to a build from before this change.
Changes
Vocabulary
indexing/extract/ontology/(8 files, ~3,100 lines): ontology loading andstructural validation, a plain-data read index that crosses the extraction process
boundary, prompt rendering, XSD coercion, and the filter.
vocabulary instead of inventing one. Composed into the templates rather than substituted
into a placeholder, so a no-ontology prompt is unchanged and existing LLM caches stay
valid.
OntologyFilter, a deterministicTransformComponentrunning after the topicextractor: it resolves each emitted name, rewrites resolved names to the authored
spelling, drops what does not conform, and annotates survivors so the build stage needs no
ontology knowledge of its own.
ontology_authority='off'says nothing;'align'(thedefault) stores a matched concept under the ontology's name and keeps what the ontology
does not declare;
'strict'also excludes what does not conform. The level resolves to sixindependent dimensions, each individually overridable, so a single gate can be asked for by
name.
vocabulary_format:'prose'(default) renders three labelled sections, one peroutput channel;
'turtle'shows the ontology's own source.Typing
bounds, decimals, booleans, dates, times and URIs. Lenient about lexical form — digit
grouping, a zero fraction, named-month dates — and strict about meaning: a literal that
would need interpreting rather thahan stored under a key whose
name promises a number. Every value that coerces is JSON-serializable, because it travels
through node metadata into Cypher parameters.
typed_propertieschooses where:'off'(default) writesnothing,
'subject'keys it from the property's own local name on the subject entity,'complement'writestyped_value/datatypeon the value node,'both'does each toits own node.
enforce_datatypesmakes typing authoritative at'strict': a literal that does notparse as its declared type takes the fact with it, rather than being stored untyped.
entity insert is byte-identical at every placement and
value,search_strandclasscannot be disturbed. An ontology declaring a property name the graph model already owns is
refused at configuration time rath
Also
lexical_graph_index.py,build_pipeline.py,entity_graph_builder.py,local_entity_rewrites_graph_builder.py,config.py,constants.py,prompts.pyand tnorecursedirscontainsbuild, sotests/unit/indexing/build/had never been collected in a full-suite or CIrun. Removing it surfaced six buil passed, plus five tests
elsewhere already failing on main.es of test changes with
nothing to do with ontologies.
Problem
Extraction is by default only lightly guided: the LLM is seeded with preferred entity
classifications and asked to prefer are unguided altogether.
That keeps recall high, but the same concept can arrive under several names, which costs
retrieval precision and makes the graph hard to query directly.
Extracted attribute values have the a founding year or a
revenue figure is stored as the textng downstream can filter,
sort or compare on it. There was no way to say "these are the types and relationships I
want", and no way to have a declared datatype land as a native graph property.
Related issue (if any): none
Testing
Everything is driven from hand-built inputs and a small fixture ontology; no recorded
model output is checked in.
during development and deliberately removed: it required a specific Neptune Analytics
graph, so it would have been p
pytest)pre-existing live testsgated on
NEO4J_TEST_URI/Ss own invocation including--cov-fail-under=56`; total coverage 73.74%. The new package is at 100% of statementson six of seven modules and 99.4% on the seventh.
development, with typed properties confirmed present via
CALL neptune.graph.pg_schema(). Documentation site builds clean.Checklist
docs-site/src/content/docs/lextraction.mdx, plus theontologyparameter in the Indexing page'off'at every layer;typed_propertiesis delibera, so no ambientconfiguration can turn on graph writes. Enabling an ontology is a re-index boundary
(
normalize_namescan changeentity.class, which feeds the entity id), and th documented.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.